feat(rust): implement true anyOf support with OR semantics#21915
Closed
timvw wants to merge 15 commits intoOpenAPITools:masterfrom
timvw:refactoring/rust-true-anyof-support
Closed
feat(rust): implement true anyOf support with OR semantics#21915timvw wants to merge 15 commits intoOpenAPITools:masterfrom timvw:refactoring/rust-true-anyof-support
timvw wants to merge 15 commits intoOpenAPITools:masterfrom
timvw:refactoring/rust-true-anyof-support
Conversation
- anyOf now generates struct with optional fields instead of enum - Each anyOf option becomes an optional field prefixed with 'as_' - Added validate_any_of() method to ensure at least one field is set - Maintains semantic difference between anyOf (OR) and oneOf (XOR) - oneOf continues to use untagged enums (exactly one match) - anyOf uses struct with optional fields (one or more matches possible) BREAKING CHANGE: anyOf schemas now generate different code structure. Previously generated enums, now generates structs with optional fields to properly support OpenAPI anyOf semantics where multiple schemas can validate simultaneously.
- Added comprehensive test spec covering various oneOf/anyOf scenarios - Tests simple primitives, complex objects, and nested schemas - Tests oneOf with and without discriminator - Tests anyOf with overlapping properties - Added documentation explaining semantic differences - Demonstrates that oneOf picks first matching (untagged enum) - Demonstrates that anyOf allows multiple matches (struct with optional fields) The tests verify: - oneOf generates enums (XOR semantics) - anyOf generates structs with optional fields (OR semantics) - oneOf with discriminator generates tagged enums - anyOf validation ensures at least one field is set
- Added links to OpenAPI 3.1.0 spec for oneOf/anyOf definitions - Added links to JSON Schema documentation - Added comprehensive comparison of how different languages handle oneOf/anyOf - Documented how Java, TypeScript, Python, Go, and C# handle these constructs - Explained how each language deals with untagged union deserialization - Added comparison table showing implementation approaches Key findings: - Most languages handle oneOf/anyOf at runtime with custom deserializers - TypeScript uses native union types for both (no semantic difference) - Only Rust and Python truly differentiate anyOf (OR) from oneOf (XOR) - Java uses AbstractOpenApiSchema with TypeAdapters for both - All implementations try types in order for untagged unions
Member
|
when it's ready, can you please pull my PR (branch) into yours to include tests for anyOf? ref: #21911 |
- Documented that NO generator refuses to generate for ambiguous schemas - Added detailed breakdown of each language's ambiguity handling strategy - Documented Swift's oneOfUnknownDefaultCase option for unmatched values - Explained Python's strict ValidationError approach - Documented Java's pragmatic 'first match wins' approach - Added common warnings and limitations from the codebase - Included best practices for avoiding ambiguity (discriminators, mutual exclusion, ordering) - Explained why generators choose fallbacks over refusing generation Key findings: - All generators have fallback strategies rather than refusing - Python has the strictest validation (ValidationError for multiple oneOf matches) - TypeScript is the most permissive (structural typing, no runtime validation) - Most use 'first match wins' for untagged unions - Pragmatism wins over strictness due to real-world API imperfections
- Added analysis of whether untagged enums violate OpenAPI/JSON Schema spec
- Documented that discriminator is optional ('when used') not required
- Clarified that oneOf MUST validate exactly one match per JSON Schema
- Documented that 'first match wins' violates strict spec compliance
- Added compliance table showing Python as only fully compliant implementation
- Included example of what fully compliant Rust code would look like
Key finding: Current implementations prioritize pragmatism over strict compliance.
Most generators (including Rust) use 'first match wins' which technically
violates the oneOf requirement that exactly one schema must match.
The spec allows validation errors, and strictly speaking, generators should
validate all schemas to ensure exactly one matches for oneOf.
- Added allOf as required composition (struct with all required fields) - Clarified anyOf as optional composition (struct with optional fields) - Confirmed oneOf as exclusive choice (enum) - Added comprehensive comparison table of all three keywords - Included correct Rust implementations for each - Documented the fundamental pattern: composition vs choice Key insight: - allOf and anyOf are BOTH compositional (merge schemas) - allOf requires ALL fields, anyOf allows optional combinations - oneOf is NOT compositional (exclusive choice) Most generators incorrectly treat anyOf like oneOf (choice) when it should be compositional like allOf but with optional fields.
Member
my suggestion is to focus on just this change (fix anyOf) to start with |
…penapi-generator into refactoring/rust-true-anyof-support
…Of support - Merged PR #21911 from wing328 which adds anyOf test samples - Regenerated samples with our new anyOf implementation - Models now correctly generate as structs with optional fields (OR semantics) - ModelIdentifier and AnotherAnyOfTest demonstrate proper anyOf behavior Co-Authored-By: William Cheng <wing328hk@gmail.com>
Documented the real-world implications of the anyOf fix by showing: - How the old generator would produce duplicate enum variants (broken Rust code) - Why wing328's test case perfectly demonstrates the problem - The practical difference between treating anyOf as choice vs composition These findings emerged from merging PR #21911 and seeing firsthand how the old behavior would literally generate uncompilable code for certain anyOf schemas.
Created detailed documentation covering: - Current type mappings from OpenAPI to Rust - How oneOf, anyOf, and allOf are handled (or not) - Alternative approaches that were considered - Configuration options and their impact - Known limitations and rationale for decisions - Proposed changes in PR #21915 for true anyOf support This documentation provides the context needed to understand both the current implementation and the proposed improvements.
Moved from docs/ to docs/generators/ where generator-specific documentation belongs, alongside rust.md, rust-server.md, etc.
Enhanced the type mapping documentation with: - Alternative composition approach for allOf using serde flatten - Explanation of how to avoid property name conflicts - Generated names for anonymous objects (Object$1, Object$2) - Trade-offs between flattening vs composition approaches This approach would maintain type safety while avoiding the complex property merging issues that arise with allOf schemas.
Added comprehensive comparison showing how different strongly-typed languages handle these OpenAPI constructs: - Java: Interface/inheritance approach, treats anyOf as oneOf - TypeScript: Union/intersection types (most natural fit) - Go: Explicit struct with pointers and custom unmarshaling - C#: Abstract classes and polymorphic serialization - Python: Union types with runtime validation - Swift: Enums with associated values (perfect for oneOf) Key findings: - No language correctly implements anyOf semantics (one or more) - Most treat anyOf identical to oneOf (exactly one) - TypeScript's type system is best suited for these constructs - Rust's proposed struct approach is reasonable given its constraints This comparison validates that the anyOf problem is industry-wide, not specific to Rust, and that our proposed solution is pragmatic.
Member
|
all tests passed. @timvw is this PR ready for review? |
Contributor
Author
|
No... I hope to find some time to work on this coming weekend... |
Member
|
FYI. There's another PR to add anyOf support to the rust-axum server generator: #21948. Please review when you've time to see if you like the implementation in that PR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR implements true anyOf support for the Rust generator, properly differentiating it from oneOf semantics as defined in the OpenAPI specification.
Key Changes
OpenAPI Specification Compliance
Implementation Details
validate_any_of()method to ensure at least one field is setTesting
Documentation
Breaking Changes
Related Issues
PR checklist
master